/**
 * This code is an implementation of Alea algorithm; (C) 2010 Johannes Baagøe.
 * Alea is licensed according to the http://en.wikipedia.org/wiki/MIT_License.
 */

const FRAC = 2.3283064365386963e-10; /* 2^-32 */

class RNG {
	_seed = 0;
	_s0 = 0;
	_s1 = 0;
	_s2 = 0;
	_c = 0;

	getSeed() { return this._seed; }

	/**
	 * Seed the number generator
	 */
	setSeed(seed: number) {
		seed = (seed < 1 ? 1/seed : seed);

		this._seed = seed;
		this._s0 = (seed >>> 0) * FRAC;

		seed = (seed*69069 + 1) >>> 0;
		this._s1 = seed * FRAC;

		seed = (seed*69069 + 1) >>> 0;
		this._s2 = seed * FRAC;

		this._c = 1;
		return this;
	}

	/**
	 * @returns Pseudorandom value [0,1), uniformly distributed
	 */
	getUniform() {
		let t = 2091639 * this._s0 + this._c * FRAC;
		this._s0 = this._s1;
		this._s1 = this._s2;
		this._c = t | 0;
		this._s2 = t - this._c;
		return this._s2;
	}

	/**
	 * @param lowerBound The lower end of the range to return a value from, inclusive
	 * @param upperBound The upper end of the range to return a value from, inclusive
	 * @returns Pseudorandom value [lowerBound, upperBound], using ROT.RNG.getUniform() to distribute the value
	 */
	getUniformInt(lowerBound: number, upperBound: number) {
		let max = Math.max(lowerBound, upperBound);
		let min = Math.min(lowerBound, upperBound);
		return Math.floor(this.getUniform() * (max - min + 1)) + min;
	}

	/**
	 * @param mean Mean value
	 * @param stddev Standard deviation. ~95% of the absolute values will be lower than 2*stddev.
	 * @returns A normally distributed pseudorandom value
	 */
	getNormal(mean = 0, stddev = 1) {
		let u, v, r;
		do {
			u = 2*this.getUniform()-1;
			v = 2*this.getUniform()-1;
			r = u*u + v*v;
		} while (r > 1 || r == 0);

		let gauss = u * Math.sqrt(-2*Math.log(r)/r);
		return mean + gauss*stddev;
	}

	/**
	 * @returns Pseudorandom value [1,100] inclusive, uniformly distributed
	 */
	getPercentage() {
		return 1 + Math.floor(this.getUniform()*100);
	}

	/**
	 * @returns Randomly picked item, null when length=0
	 */
	getItem<T>(array: Array<T>) {
		if (!array.length) { return null; }
		return array[Math.floor(this.getUniform() * array.length)];
	}

	/**
	 * @returns New array with randomized items
	 */
	shuffle<T>(array: Array<T>) {
		let result = [];
		let clone = array.slice();
		while (clone.length) {
			let index = clone.indexOf(this.getItem(clone) as T);
			result.push(clone.splice(index, 1)[0]);
		}
		return result;
	}

	/**
	 * @param data key=whatever, value=weight (relative probability)
	 * @returns whatever
	 */
	getWeightedValue(data: { [key: string]: number, [key: number]: number }) {
		let total = 0;
		
		for (let id in data) {
			total += data[id];
		}
		let random = this.getUniform()*total;
		
		let id, part = 0;
		for (id in data) {
			part += data[id];
			if (random < part) { return id; }
		}

		// If by some floating-point annoyance we have
		// random >= total, just return the last id.
		return id;
	}

	/**
	 * Get RNG state. Useful for storing the state and re-setting it via setState.
	 * @returns Internal state
	 */
	getState() { return [this._s0, this._s1, this._s2, this._c]; }

	/**
	 * Set a previously retrieved state.
	 */
	setState(state: number[]) {
		this._s0 = state[0];
		this._s1 = state[1];
		this._s2 = state[2];
		this._c  = state[3];
		return this;
	}

	/**
	 * Returns a cloned RNG
	 */
	clone() {
		let clone = new RNG();
		return clone.setState(this.getState());
	}
}

export default new RNG().setSeed(Date.now());
